package org.mayanjun.code.easyweb.springmvc.config;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.mayanjun.code.easyweb.common.ServiceException;
import org.mayanjun.code.easyweb.springmvc.annotation.ScanIgnore;
import org.mayanjun.code.easyweb.springmvc.interceptor.AnnotationBasedProcessorInterceptor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.beans.factory.config.PropertiesFactoryBean;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.context.annotation.*;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternUtils;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.converter.xml.MarshallingHttpMessageConverter;
import org.springframework.oxm.Marshaller;
import org.springframework.oxm.Unmarshaller;
import org.springframework.oxm.castor.CastorMarshaller;
import org.springframework.web.multipart.MultipartResolver;
import org.springframework.web.multipart.commons.CommonsMultipartResolver;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.*;
import org.springframework.web.servlet.view.ContentNegotiatingViewResolver;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
import org.springframework.web.servlet.view.JstlView;
import org.springframework.web.servlet.view.velocity.VelocityConfigurer;
import org.springframework.web.servlet.view.velocity.VelocityViewResolver;

import java.io.IOException;
import java.nio.charset.Charset;
import java.util.*;

/**
 * Spring web mvc configuration
 * All the required components for Spring MVC is defined here.
 * This class have a responsibility as well to load the web.properties config file
 * @author mayanjun
 */
@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = true)
@PropertySource(value = Global.MVC_CONFIG_LOCATION, ignoreResourceNotFound = true)
@ScanIgnore
public class WebMvcApplicationConfig extends WebMvcConfigurerAdapter implements ResourceLoaderAware {

	private static final Logger LOG = LoggerFactory.getLogger(WebMvcApplicationConfig.class);
	private static PropertiesResourcesLogger propertiesResourcesLogger = new PropertiesResourcesLogger(LOG);
	private ResourceLoader resourceLoader;
	private Properties webProperties;

	@Bean(name = Global.BEAN_NAME_ROOT_PROPS)
	public PropertiesFactoryBean getPropertiesFactoryBean() {
		PropertiesFactoryBean bean = new PropertiesFactoryBean();
		Resource res[] = getPropsResources(resourceLoader);
		bean.setLocations(res);
		return bean;
	}

	protected static Resource[] getPropsResources(ResourceLoader loader) {
		PropertySource source = WebMvcApplicationConfig.class.getAnnotation(PropertySource.class);
		if(source != null) {
			String[] props = source.value();
			List<Resource> list = new ArrayList<Resource>();
			for(int i = 0; i < props.length; i++) {
				String prop = props[i];
				Resource r = loader.getResource(prop);
				if(r.exists()) {
					list.add(r);
				}
			}
			if(CollectionUtils.isNotEmpty(list)) {
				Resource res[] = new Resource[list.size()];
				list.toArray(res);
				propertiesResourcesLogger.log(Global.BEAN_NAME_ROOT_PROPS, res);
				return res;
			}
		}
		return null;
	}

	@Bean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME)
	public MultipartResolver getMultipartResolver() {
		CommonsMultipartResolver r = new CommonsMultipartResolver();
		long ioSize = -1;

		String size = getWebProperty(WebPropertiesNames.IO_UPLOAD_SIZE);
		if(StringUtils.isNotBlank(size)) {
			try {
				ioSize = Long.parseLong(size);
			} catch (NumberFormatException e) {
				LOG.warn("Invalid value for 'io.upload.size': {}, must be a numeric", size);
			}
			if(ioSize < 0) ioSize = -1;
		}

		r.setMaxUploadSize(ioSize);
		LOG.info("File upload max size is set to {} bytes", ioSize);

		return r;
	}

	/**
	 * Returns a property defined in web.properties
	 * @param name
	 * @return
	 * @throws Exception
     */
	private String getWebProperty(String name)  {
		if(webProperties == null) {
			webProperties = new Properties();
			Resource res = resourceLoader.getResource(Global.MVC_CONFIG_LOCATION);
			try {
				if(res.exists()) webProperties.load(res.getInputStream());
			} catch (IOException e) {
				LOG.warn("No web.properties found");
			}
		}
		return webProperties.getProperty(name);
	}

	@Bean
	@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
	public Marshaller getXMLMarshaller() throws Exception {
		CastorMarshaller marshaller = new CastorMarshaller();
		marshaller.setEncoding("utf-8");

		Resource[] rs = getValidCastorMappingLocations();
		if(rs != null) marshaller.setMappingLocations(getCastorMappingLocations());
		return marshaller;

	}

	@Bean
	@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
	public Unmarshaller getXMLUnmarshaller() throws Exception {
		return (Unmarshaller) getXMLMarshaller();
	}

	private Resource[] getValidCastorMappingLocations() throws Exception {
		Resource res[] = getCastorMappingLocations();
		List<Resource> list = new ArrayList<Resource>();
		if(res != null && res.length > 0) {
			for(Resource r : res) if(r.exists() && r.isReadable()) list.add(r);

			if(!list.isEmpty()) {
				Resource rtn[] = new Resource[list.size()];
				return list.toArray(rtn);
			}
		}
		return null;
	}

	@Bean
	public Resource[] getCastorMappingLocations() throws Exception {
		String locations = getWebProperty(WebPropertiesNames.UNMARSHALLER_CASTOR_MAPPING_LOCATIONS);
		if(StringUtils.isNotBlank(locations)) {
			LOG.info("Using castor mappings: {}", locations);
			String locationsArray[] = locations.split(",");
			ResourcePatternResolver resolver = ResourcePatternUtils.getResourcePatternResolver(resourceLoader);
			ArrayList<Resource> list = new ArrayList<Resource>();
			for(String s : locationsArray) {
				Resource res[] = resolver.getResources("classpath:" + s);
				if(res != null && res.length > 0) list.addAll(Arrays.asList(res));
			}
			if(!list.isEmpty()) {
				Resource res[] = new Resource[list.size()];
				return list.toArray(res);
			}
		}

		LOG.info("Using default castor mappings: {}" ,Global.DEFAULT_CASTOR_MAPPING_LOCATIONS);
		return new Resource[]{
				resourceLoader.getResource(Global.DEFAULT_CASTOR_MAPPING_LOCATIONS)
		};
	}

	@Override
	public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {

		MappingJackson2HttpMessageConverter c0 = new MappingJackson2HttpMessageConverter();
		ObjectMapper mapper = new ObjectMapper();
		mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
		c0.setObjectMapper(mapper);

		StringHttpMessageConverter c1 = new StringHttpMessageConverter(Charset.defaultCharset());
		List<MediaType> supportedMediaTypes = new ArrayList<MediaType>();
		supportedMediaTypes.add(MediaType.valueOf("text/plain;charset=UTF-8"));
		c1.setSupportedMediaTypes(supportedMediaTypes);

		MarshallingHttpMessageConverter c2 = new MarshallingHttpMessageConverter();
		try {
			Marshaller marshaller = getXMLMarshaller();
			c2.setMarshaller(marshaller);
			LOG.info("XML marshaller created: {}", marshaller);

			Unmarshaller unMarshaller = getXMLUnmarshaller();
			c2.setUnmarshaller(unMarshaller);
			LOG.info("XML unmarshaller created: {}", unMarshaller);
		} catch (Exception e) {
			ServiceException se = new ServiceException("Can not initialize castor (un)marshaller:" + e.getMessage());
			se.initCause(e);
			throw se;
		}

		converters.add(c0);
		converters.add(c1);
		converters.add(c2);
	}

	@Override
	public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
		configurer.defaultContentType(MediaType.APPLICATION_JSON)
				.favorParameter(true)
				.parameterName("_format")
				.favorPathExtension(false)
				.ignoreAcceptHeader(false);

		Map<String, MediaType> mediaTypes = new HashMap<String, MediaType>();
		mediaTypes.put("json", MediaType.APPLICATION_JSON);
		mediaTypes.put("xml", MediaType.APPLICATION_XML);
		mediaTypes.put("htm", MediaType.TEXT_HTML);
		mediaTypes.put("html", MediaType.TEXT_HTML);
		mediaTypes.put("jsp", MediaType.TEXT_HTML);
		configurer.mediaTypes(mediaTypes);
	}

	@Override
	public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
		long timeout = Global.DEFAULT_SPRING_ASYNC_TIMEOUT;
		try {
			String timeoutString  = getWebProperty(WebPropertiesNames.SPRING_ASYNC_TIMEOUT);
			timeout = Long.parseLong(timeoutString);
		} catch (Exception e){}
		LOG.info("Using spring async timeout: {} milliseconds", timeout);
		configurer.setDefaultTimeout(timeout);
		super.configureAsyncSupport(configurer);
	}

	@Override
	public void addInterceptors(InterceptorRegistry registry) {
		registry.addInterceptor(getAnnotationBasedInterceptor());
		LOG.info("Interceptor initialized");
	}

	@Bean
	public AnnotationBasedProcessorInterceptor getAnnotationBasedInterceptor() {
		return new AnnotationBasedProcessorInterceptor();
	}

	// Config message converters

	@Override
	public void addResourceHandlers(ResourceHandlerRegistry registry) {
		registry.addResourceHandler("/resources/**").addResourceLocations("/resources/");
	}

	// Config view resolvers

	@Override
	public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
		configurer.enable();
	}

	@Bean
	public VelocityViewResolver getVelocityViewResolver() {
		VelocityViewResolver bean = new VelocityViewResolver();
		bean.setCache(true);
		bean.setPrefix("");
		bean.setSuffix(Global.VELOCITY_VIEW_SUFFIX);
		bean.setExposeRequestAttributes(true);
		bean.setContentType("text/html;charset=UTF-8");
		bean.setRequestContextAttribute("requestContext");

		String velocityToolBoxLocation = getWebProperty(WebPropertiesNames.VELOCITY_TOOLBOX_LOCATION);
		if(StringUtils.isNotBlank(velocityToolBoxLocation)) {
			LOG.info("Using velocity toolbox location: {}", velocityToolBoxLocation);
			bean.setToolboxConfigLocation(velocityToolBoxLocation);
		}
		bean.setOrder(1);
		return bean;
	}

	@Bean
	public VelocityConfigurer getVelocityConfigurer() {
		VelocityConfigurer bean = new VelocityConfigurer();
		bean.setResourceLoaderPath(Global.VELOCITY_VIEW_LOCATION);
		LOG.info("Using velocity view location: {}", Global.VELOCITY_VIEW_LOCATION);
		bean.setVelocityPropertiesMap(new HashMap<String,Object>(){
			private static final long serialVersionUID = 8750248331737543258L;
			{
				this.put("input.encoding", "utf-8");
				this.put("output.encoding", "utf-8");
			}});
		return bean;
	}

	@Bean
	public ContentNegotiatingViewResolver getContentNegotiatingViewResolver() {
		ContentNegotiatingViewResolver resolver = new ContentNegotiatingViewResolver();
		resolver.setOrder(2);
		return resolver;
	}

	@Bean
	public InternalResourceViewResolver getInternalResourceViewResolver() {
		InternalResourceViewResolver resolver = new InternalResourceViewResolver();
		resolver.setPrefix("/WEB-INF/views/jsp/");
		resolver.setSuffix(".jsp");
		resolver.setViewClass(JstlView.class);
		resolver.setCache(false);
		resolver.setOrder(3);
		return resolver;
	}

	@Override
	public void setResourceLoader(ResourceLoader resourceLoader) {
		this.resourceLoader = resourceLoader;
	}
}